/**
 * 打包程序
 * 1.首先遍历所有需要打包的程序，获取一个map
 * key(路径)：value(文件的状态) 
 * 2.计算生成文件的配置
 * key("相对路径")：value（开始位置，结束位置）
 * 3.获取配置的大小
 * 4.整个打包程序分为3个部分，配置大小，配置，文件资源区
 */
import fs from "fs";
import Module from "module";
import path from "path";
import md5 from "./md5"

// 打包这个文件夹下的所有文件  
// 外部依赖如果是第三方库，则需要把第三方库全部打包，不然一些资源没有读取到会报错
export default class packag{
    // 项目根目录
    static basePath = "";
    // 依赖包
    static relyOn = new Set<{
        // 文件路径
        path:string;
        // 打包后的文件路径
        packageFilePath:string;
        // 文件属于零散的外部依赖还是库级依赖
        type:string;
        // 包级或零散依赖的根目录
        relyingOnRootDirectory:string; 
        // 打包前缀
        packagePrefix:string;
    }>();
    // 已经依赖好的目录
    static complet = [];
    // 依赖库的地址
    static libArray = [];
    // 过滤后缀
    static filterSuffix = [];

    static async start(address:string){
        this.basePath = this.standardDelimiter(path.resolve(address));
        // this.libArray.push(this.basePath);
        if(fs.existsSync(path.parse(this.basePath).name+".nar")){
            fs.unlinkSync(path.parse(this.basePath).name+".nar");
        }
        console.log("开始加载项目文件");
        let stateObj = this.getAllState(this.standardDelimiter(address),undefined,this.basePath,"");
        console.log("项目执行文件加载完成");
        console.log("项目资源文件加载完成");
        console.log("开始加载外部依赖")
        // 遍历依赖，打包依赖
        // 外部依赖不检测md5
        while(true){
            let brea = true;
            for(let e of this.relyOn){
                if(!stateObj[e.packageFilePath]){
                    // 进入这里的都是外部依赖
                    brea = false;
                    // 加载依赖
                    let state:any = fs.statSync(e.path);
                    // 判断是不是库级依赖，如果是就把整个库的依赖打包
                    // 判断调用这个库的方式是否为包名调用,如果是，就打包到node_modules中
                    // 属于库级依赖
                    if(e.relyingOnRootDirectory){
                        // 把包中的资源全部打包
                        // console.log(e)
                        this.getAllState(e.relyingOnRootDirectory,stateObj,e.relyingOnRootDirectory,e.packagePrefix);
                        // console.log(stateObj[e.packageFilePath] !== undefined)
                        // console.log();
                        
                    }else{
                        // console.log(e)
                        // 属于零散依赖
                        let {content, guidePackage} = this.parsFileContent(e.path,path.dirname(path.dirname(e.path)));
                        state.content = content;
                        state.guidePackage = guidePackage;
                        state.size = Buffer.from(state.content).length;
                        state.filepath = e.path;
                        // console.log("sss",[e.packageFilePath])
                        stateObj[e.packageFilePath] = state;
                    }
                }
            }
            if(brea) break;
        }
        console.log("外部依赖加载完成");
        console.log("开始生成配置");
        let {mainPackage} = this.generateConfig(stateObj);
        console.log("开始打包");
        await this.performPackag(stateObj,mainPackage,path.parse(this.basePath).name+".nar");
        console.log()
        console.log("打包完成");
    }
    // 获取所有文件的位置
    static getAllState(drr:string,map={},basePath=this.basePath,prefix=""){
        // 如果属于已加载后的文件夹则不进行加载
        if(this.libArray.includes(drr)) return undefined;

        this.libArray.push(drr);
        console.log("加载: ",drr)
        // 遍历这个文件夹
        fs.readdirSync(drr).forEach((e:string)=>{
            let filepath = this.standardDelimiter(path.resolve(drr,e));
            if(map[this.standardDelimiter(path.join(prefix,filepath.replace(basePath,"")))]) return;
            // 判断是文件夹还是文件
            let state:any = fs.statSync(path.join(drr,e));
            if(state.isFile()){
                // 指定后缀的不进行打包
                if(this.filterSuffix.some(suff=>e.endsWith(suff))) return;
                if(e.endsWith(".js")){
                    let {content, guidePackage,md5} = this.parsFileContent(filepath,basePath,this.standardDelimiter(path.join(prefix,filepath.replace(basePath,""))),prefix);
                    state.content = content;
                    state.guidePackage = guidePackage;
                    md5 && (state.md5 = md5);
                    state.size = Buffer.from(state.content).length;
                }
                state.filepath = filepath;
                // 使用相对路径作为key
                // console.log([this.standardDelimiter(path.join(prefix,filepath.replace(basePath,"")))])
                map[this.standardDelimiter(path.join(prefix,filepath.replace(basePath,"")))] = state;
            }else{
                this.getAllState(filepath,map,basePath,prefix);
            }
        });

        return map;
    }

    /**
     * 获取外部依赖的最外层文件夹
     * @param filePath 
     * @returns 
     */
    static getMaxRelyOnPackage(filePath:string){
        while(true){
            let p = path.dirname(filePath);
            if(p === filePath) return null;
            if(fs.existsSync(p+"/package.json")){
                // 判断package.json中的name是不是@开头
                let packjson = require(p+"/package.json");
                if(/^@.*?\//.test(packjson.name || "")){
                    return {
                        base:this.standardDelimiter(path.dirname(p)),
                        child:"/"+path.basename(p)
                    };
                }
                return {
                    base:this.standardDelimiter(p),
                    child:""
                };
            }
            filePath = p;
        }
    }

    /**
     * 分隔符标准化 /
     * @param str 
     */
    static standardDelimiter(str:string){
        return str.replace(/[\/\\]+/g,"/");
    }

    /**
     * 
     * @param filePath 
     * @param alias 
     * @returns 
     */
    static getLayer(alias:string){
        return "./"+[path.dirname(alias).split(/[\\\/]+/).filter(e=>e).map(e=>"../").join("")]
    }

    // 解析文件里面导入的模块地址，并且进行更改
    static parsFileContent(filePath:string,basePath="",alias="",prefix=""){
        // 读取文件
        let content = fs.readFileSync(filePath,"utf8");
        let _md5 = md5(content);

        // 先把注释匹配清空 /\*[^]*?\*/|//.*
        let annotation = content.match(/\/\*[^]*?\*\/|\/\/.*/g) || [];
        // 清空注解
        content = content.replace(/\/\*[^]*?\*\/|\/\/.*/g,"2021.12.11.21.44");
        /**
        * 
        */
        let guidePackage = content.match(/(?<=require\(["']).*?(?=["']\))/g) || [];
        // 正则匹配出所有导入的模块
        content.match(/(?<=require\(["']).*?(?=["']\))/g)?.forEach((e:string,key:number)=>{
            try{
                let a = e;
                if(e.startsWith("."))a = path.join(path.dirname(path.resolve(filePath)),e);
                a = this.standardDelimiter(require.resolve(a,{
                    // @ts-ignore
                    paths:Module._nodeModulePaths(path.dirname(path.resolve(filePath)) )
                })).replace(basePath,"");
                // 对模块导入的模块进行解析
                if(guidePackage[key] === a) {
                    guidePackage[key] = undefined;
                    return
                }
                let info = {
                    // 文件路径
                    path:a,
                    // 打包后的文件路径
                    packageFilePath:undefined,
                    // 文件属于零散的外部依赖还是库级依赖
                    type:undefined,
                    // 包级或零散依赖的根目录
                    relyingOnRootDirectory:undefined,
                    // 打包前缀
                    packagePrefix:""
                }
                // 依赖文件存在 添加到依赖中
                if(fs.existsSync(a)){this.relyOn.add(info);}

                if(/[A-Z]:/.test(a) || fs.existsSync(a)){
                    let packagePath = this.getMaxRelyOnPackage(a);
                    // 这是一个外部依赖，保存其引用名称
                    if(/^[.\/\\]/.test(e)){
                        // 获取其父级文件夹
                        // 以相对路径引用的文件就需要消除其md5值，因为后期还原这个引用路径值会变
                        _md5 = undefined;
                        // 更改原始文件的导入位置
                        if(packagePath){
                            a = this.standardDelimiter(["/lib",path.basename(packagePath.base)+packagePath.child,a.replace(packagePath.base,"")].join("/"));
                            
                            // 包的根目录
                            info.packagePrefix = this.standardDelimiter(["/lib",path.basename(packagePath.base)+packagePath.child].join("/"));
                        }else{
                            a = this.standardDelimiter(["/lib",path.basename(path.dirname(a)),path.basename(a)].join("/"));
                            // 包的前缀
                            info.packagePrefix = this.standardDelimiter(["/lib",path.basename(packagePath.base)+packagePath.child].join("/"));
                        }
                        info.type = "lib";
                        info.packageFilePath = a;

                        if(!this.libArray.some(e=>{
                            return e.startsWith(this.standardDelimiter(path.join(packagePath.base,packagePath.child)));
                        }) ){
                            info.relyingOnRootDirectory = packagePath.base+packagePath.child;
                        }else{
                            info.relyingOnRootDirectory = undefined;
                        }
                        guidePackage[key] = this.standardDelimiter([this.getLayer(alias),a].join("/"));
                    }else{
                        // 因为用的是包名就不需要更改原始路径
                        guidePackage[key] = e; 
                        // 获取包的根目录
                        info.relyingOnRootDirectory = this.standardDelimiter(packagePath.base+packagePath.child);
                        // 设置类型
                        info.type = "node_modules";
                        // 以包名作为引用路径就不需要进行更改md5
                        // 更改原始文件位置
                        info.packagePrefix = this.standardDelimiter(["/node_modules",path.basename(packagePath.base)+packagePath.child].join("/"));
                        a = this.standardDelimiter([info.packagePrefix,path.basename(a)].join("/"));
                        // console.log(">>",[info.packagePrefix])
                        info.packageFilePath = a;
                    }
                    // 还是绝对路径，对其归为外部依赖，获取父类文件夹
                    // 获取依赖文件的最大外围
                    guidePackage[key] = this.standardDelimiter(guidePackage[key]);
                    // 判断依赖与文件之间是否是同级关系
                }else{
                    // 虽然显示不是外部依赖，但是如果是，外部依赖包中的文件就会出错
                    a=this.standardDelimiter(prefix+a);
                }
                a = this.standardDelimiter("@/"+a.replace(basePath,""));
                guidePackage[key]+=">>>"+a;
                content = content.replace(new RegExp(`(?<=require\\(["'])${e}(?=["']\\))`),a);
            }catch(err){
                // console.log([filePath])
                guidePackage[key]=undefined;
            }
        });

        let i = 0;
        content = content.replace(/2021\.12\.11\.21\.44/g,_=>{
            return annotation[i++];
        });

        return {
            content,
            guidePackage:guidePackage.filter(e=>e),
            md5:_md5
        };
    }

    // 生成配置
    static generateConfig(state:any = {}){
        let startLocation = 0;
        let mainPack = "";
        Object.keys(state).some(e=>{
            if(e.endsWith("package.json")) {
                mainPack=e; return true;
            }
            return false;
        });
        // 遍历状态
        Object.keys(state).forEach(filepath=>{
            if(filepath.endsWith("package.json") && filepath.length<mainPack.length){
                mainPack = filepath;
            }
            state[filepath].scope=`${startLocation},${startLocation+state[filepath].size}`;
            startLocation+=state[filepath].size
        });
        return {
            state,
            mainPackage:mainPack
        };
    }
    // 获取配置的大小
    static getConfigSize(a){
        return JSON.stringify(a).length;
    }

    // 写入打包文件，所有数据以\0结尾
    static async performPackag(state:any,mainPackage:string, fileName:string){
        let relative = {"mainPackage":mainPackage};
        
        Object.keys(state).forEach(e=>{
            relative[e]={...state[e]};
        });
        Object.keys(relative).forEach(e=>{
            delete relative[e].content
            delete relative[e].filepath;
        })
        // console.log(relative)
        let head = JSON.stringify(relative)+"\0";
        // 创建一个空文件
        fs.writeFileSync(path.join(process.cwd(),fileName),"");

        let write = fs.createWriteStream(path.join(process.cwd(),fileName),{
            start:0,
            flags:"r+"
        });
        write.write(head);
        write.close(); // 关闭这个流
        let total = 0;
        // 开始遍历
        await Promise.all(Object.keys(state).map(async e=>{
            // console.log("开始打包 "+e);
            if(state[e].content){
                // 内容读取完成了，可以直接进行写入
                let start = Number(state[e].scope.split(",")[0])+Buffer.from(head).length;
                let write = fs.createWriteStream(path.join(process.cwd(),fileName),{
                    start,
                    flags:"r+"
                });
                write.write(state[e].content);
                write.close();
            }else{
               await this.stream(Buffer.from(head).length,state[e],state[e].filepath,path.join(process.cwd(),fileName))
            }
            console.log("已打包："+(++total)+"个文件",e);
        }));
    }
    /**
     * 
     * @param {String} info 
     * @param {Object} state 
     * @param {WriteStream} stream 
     */
    static stream(index:number,info:any,filepath:string,targetFile:string){
        return new Promise(endThe=>{
            let start = Number(info.scope.split(",")[0])+index;
            let write = fs.createWriteStream(targetFile,{
                start,
                flags:"r+"
            });
            fs.createReadStream(filepath).pipe(write).on("close",()=>endThe(undefined));
        })
    }
}